package apig.algorithm.colmo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Iterator;
import java.util.LinkedList;

import apig.algorithm.Algorithm;
import apig.algorithm.ProgressMonitor;
import apig.algorithm.tokenizer.JavaTokenizer;
import apig.algorithm.tokenizer.MalformedCodeException;
import apig.algorithm.tokenizer.Tokenizer.Token;

/**
 * 
 * @author evgeniy
 * 
 */
public class ColmogorovDiffAlgo implements Algorithm {

	private static final int EXPECTED_ALPHABETH_SIZE = 400;
	private static final int INITIAL_BUFFER_SIZE = 1000;

	/**
	 * TODO: remake and verify
	 */
	@Override
	public void inspect(Connection c, int codeId, Iterable<Integer> codeIds,
			ProgressMonitor pm) throws SQLException {
		JavaTokenizer jt = new JavaTokenizer();
		Statement s = c.createStatement();
		ResultSet codeRS = s
				.executeQuery("SELECT codes.code FROM codes "
						+ "WHERE codes.code_id = '" + codeId + "'");
		codeRS.next();
		String code = codeRS.getString(1);
		BufferProvider bp = new BufferProvider();
		PreparedStatement hypoStatement = c
				.prepareStatement("INSERT INTO hypos values "
						+ "(nextval('hypos'), ?, ?, ?, null, null, null, null);");
		hypoStatement.setInt(1, codeId);
		try {
			Token codeTokens[] = jt.parse(code);

			Iterator<Integer> codesIt = codeIds.iterator();
			while (codesIt.hasNext()) {
				int otherCodeId = codesIt.next();
				if (otherCodeId != codeId) {
					ResultSet otherCodeRS = s
							.executeQuery("SELECT codes.code FROM codes "
									+ "WHERE codes.code_id = '"
									+ otherCodeId + "'");
					otherCodeRS.next();
					String otherCode = otherCodeRS.getString(1);
					Token[] otherCodeTokens = jt.parse(otherCode);

					double jointDiff = getJointDifficulty(codeTokens,
							otherCodeTokens, bp);
					double xDiff = getDifficulty(codeTokens, bp);
					double xCondYDiff = getConditionalDifficulty(codeTokens,
							otherCodeTokens, bp);
					double veracity = 1 - (xDiff - xCondYDiff) / jointDiff;
					// System.out.println("x diff: " + xDiff);
					// System.out.println("xcondy diff:" + xCondYDiff);
					// System.out.println("joint: " + jointDiff);
					// System.out.println("(" + codeId + ";" + otherCodeId +
					// ")veracity: " + veracity);
					// TODO: ololo, ispravit' potom, kone4no je
					if (veracity < 1) {
						hypoStatement.setInt(2, otherCodeId);
						hypoStatement.setDouble(3, veracity);
						hypoStatement.executeUpdate();
					}
				}
			}
		} catch (MalformedCodeException ex) {
			ex.printStackTrace();
		}
	}

	public static int getDifficulty(Token[] x, BufferProvider bp) {
		Dictionary d = new Dictionary();
		return getDifficulty(d, x, bp);
	}

	public static double getJointDifficulty(Token[] x, Token[] y,
			BufferProvider bp) {

		// просто закодить x и y подряд
		Dictionary d = new Dictionary();
		double xDiff = getDifficulty(d, x, bp);
		double yDiff = getDifficulty(d, y, bp);
		return xDiff + yDiff;
	}

	private static double getConditionalDifficulty(Token[] x, Token[] y,
			BufferProvider bp) {

		// create the dictionary with subwords of y
		Dictionary yD = new Dictionary();

		for (double i = 2; i < y.length; i *= 2) {
			for (int j = 0; j + i - 1 < y.length; j++) {
				yD.putWord(y, j, j + (int) i - 1);
			}
		}

		return getDifficulty(yD, x, bp);
	}

	private static int getDifficulty(Dictionary d, Token[] x, BufferProvider bp) {
		final int KEEP_SEEKING = 1;
		final int NOT_FOUND = 2;
		final int WORD_END = 3;
		int state = KEEP_SEEKING;

		double resultSize = 0;

		// need to compress x
		int nextXIndex = 0;

		// go over the word
		while (nextXIndex < x.length) {

			// check find max string from dictionary in x
			int bufferTop = -1;
			DictionaryLvl currentLvl = null;
			DictionaryLvl parentLvl = null;
			state = KEEP_SEEKING;
			while (state == KEEP_SEEKING) {
				// get the current level of dictionary
				//
				// if there is only a start: empty buffer
				if (bufferTop == -1) {
					currentLvl = d.getFirstLvl(x[nextXIndex]);
				} else { // non-empty buffer
					parentLvl = currentLvl;
					currentLvl = currentLvl.getChild(x[nextXIndex]);
				}

				// parse the gotten dictionary level
				if (currentLvl == null) {
					// the dictionary does not contain next symbol
					state = NOT_FOUND;
				} else { // the dictionary contains next symbol. keep seeking
					// add current symbol to buffer
					//
					// handle the buffer size
					if (bufferTop + 1 >= bp.buffer.length) {
						int newBuffer[] = new int[2 * bp.buffer.length];
						copy(newBuffer, bp.buffer, 0, 0, bufferTop + 1);
						bp.buffer = newBuffer;
					}
					bufferTop++;
					bp.buffer[bufferTop] = x[nextXIndex].getID();
					nextXIndex++;
				}
				if (nextXIndex >= x.length) {
					state = WORD_END;
				}
			}

			if (state == WORD_END) {
				// encode current buffer
				// resultSize += Math.log((double) currentLvl.ccn);
				resultSize++;
			} else if (state == NOT_FOUND) {
				// encode current buffer
				// resultSize += Math.log((double) parentLvl.ccn);
				resultSize++;
				// enlarge the dictionary;
				DictionaryLvl newLvl = parentLvl.putChild(x[nextXIndex]);
				newLvl.ccn = d.getCCN();
			} else {
				throw new Error();
			}
		}

		return (int) resultSize;
	}

	private static final int[] getIntTokens(Token[] tokens) {
		int[] intTokens = new int[tokens.length];
		for (int i = 0; i < intTokens.length; i++) {
			intTokens[i] = tokens[i].getID();
		}

		return intTokens;
	}

	private static void copy(int[] a1, int[] a2, int start1, int start2,
			int size) {
		for (int i = 0; i < size; i++) {
			a1[start1 + i] = a2[start2 + i];
		}
	}

	/**
	 * 
	 * @author evgeniy
	 * 
	 */
	private static class Dictionary {

		public DictionaryLvl firstLvls[];
		public int ccn = 0;

		public Dictionary() {
			createVirginDictionary();
		}

		public DictionaryLvl getFirstLvl(Token symbol) {
			return getFirstLvl(symbol.getID());
		}

		public DictionaryLvl getFirstLvl(int symbol) {
			for (DictionaryLvl lvl : firstLvls) {
				if (lvl.symbol == symbol) {
					return lvl;
				}
			}
			return null;
		}

		public int getCCN() {
			ccn++;
			return ccn;
		}

		public boolean putWord(Token[] word, int firstIndex, int lastIndex)
				throws IllegalArgumentException {
			if (firstIndex < 0) {
				throw new IllegalArgumentException();
			}
			if (lastIndex >= word.length) {
				throw new IllegalArgumentException();
			}

			// get the first level
			DictionaryLvl currentLvl = null;
			for (int i = 0; i < firstLvls.length; i++) {
				if (firstLvls[i].symbol == word[0].getID()) {
					currentLvl = firstLvls[i];
					break;
				}
			}
			if (currentLvl == null) {// which means it is not found
				throw new IllegalArgumentException();
			}

			// now we have the first (lvlIndex=0) level for our word; go ahead
			// for next levelz
			int lvlIndex = 1;
			DictionaryLvl prevLvl = null;
			while (lvlIndex < lastIndex) {
				prevLvl = currentLvl;
				currentLvl = currentLvl.getChild(word[lvlIndex]);
				if (currentLvl == null) {
					currentLvl = prevLvl.putChild(word[lvlIndex]);
					currentLvl.ccn = getCCN();
				}
				lvlIndex++;
			}

			return true;
		}

		/**
		 * 
		 * @param word
		 * @param lastIndex
		 *            - the index of the last symbol
		 * @return false if the word[0..lastIndex] is in the dictionary
		 * @return true if the word[0..lastIndex] is not in the dictionary +
		 *         adds it into the dictionary
		 * @throws IllegalArgumentException
		 *             if the word[0..lastIndex-1] is not in the dictionary
		 * @deprecated
		 */
		public boolean putWord(int word[], int firstIndex, int lastIndex)
				throws IllegalArgumentException {

			if (firstIndex < 0) {
				throw new IllegalArgumentException();
			}
			if (lastIndex >= word.length) {
				throw new IllegalArgumentException();
			}

			// get the first level
			DictionaryLvl currentLvl = null;
			for (int i = 0; i < firstLvls.length; i++) {
				if (firstLvls[i].symbol == word[0]) {
					currentLvl = firstLvls[i];
					break;
				}
			}
			if (currentLvl == null) {// which means it is not found
				throw new IllegalArgumentException();
			}

			// now we have the first (lvlIndex=0) level for our word; go ahead
			// for next levelz
			int lvlIndex = 1;
			DictionaryLvl prevLvl = null;
			while (lvlIndex < lastIndex) {
				prevLvl = currentLvl;
				currentLvl = currentLvl.getChild(word[lvlIndex]);
				if (currentLvl == null) {
					currentLvl = prevLvl.putChild(word[lvlIndex]);
					currentLvl.ccn = getCCN();
				}
				lvlIndex++;
			}

			return true;
		}

		public void createVirginDictionary() {
			firstLvls = new DictionaryLvl[EXPECTED_ALPHABETH_SIZE];
			for (int i = 0; i < firstLvls.length; i++) {
				firstLvls[i] = new DictionaryLvl(i);
				firstLvls[i].ccn = getCCN();
			}
		}

	}

	/**
	 * 
	 * @author evgeniy
	 * 
	 */
	private static class DictionaryLvl {

		// symbol of this level
		public int symbol;
		// list of children levels
		public LinkedList<DictionaryLvl> lvls = new LinkedList<DictionaryLvl>();
		public int ccn = 0;

		public DictionaryLvl(int symbol) {
			this.symbol = symbol;
		}

		public DictionaryLvl putChild(int symbol) {
			DictionaryLvl result;
			DictionaryLvl ifExistsChild;
			ifExistsChild = getChild(symbol);
			if (ifExistsChild == null) {
				result = new DictionaryLvl(symbol);
				lvls.add(result);
				return result;
			} else {
				return ifExistsChild;
			}
		}

		public DictionaryLvl putChild(Token symbol) {
			return putChild(symbol.getID());
		}

		public DictionaryLvl getChild(int symbol) {
			for (DictionaryLvl currentChildLvl : lvls) {
				if (currentChildLvl.symbol == symbol) {
					return currentChildLvl;
				}
			}
			return null;
		}

		public DictionaryLvl getChild(Token symbol) {
			return getChild(symbol.getID());
		}
	}

	private static class BufferProvider {
		int[] buffer;

		public BufferProvider() {
			buffer = new int[1024];
		}
	}
}
